package com.symaster.common.geom;

import com.symaster.common.geom.canvas.repeater.CanvasMouseListenerRepeater;
import com.symaster.common.geom.canvas.repeater.CanvasMouseMotionListenerRepeater;
import com.symaster.common.geom.entity.LineSegment;
import com.symaster.common.swing.FpsManager;
import com.symaster.common.swing.listener.CopyListener;
import com.symaster.common.swing.util.SwingUtil;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;

/**
 * @author yinmiao
 * @version 2022-07-14 14:13
 */
public class Canvas extends JComponent {

    private static final Font microsoft_yaHei = new Font("Microsoft YaHei", Font.PLAIN, 11);
    private static final Color drawLineDefaultColor = new Color(134, 134, 134);
    private Point pressedPoint;
    private boolean ctrlPressed = false;
    private final List<PolygonComponent<?>> selectList;
    private final List<OperationEntity> operations;
    private final Map<String, PolygonComponent<?>> polygonComponentMap;
    private final Map<Integer, PolygonComponent<?>> polygonComponentMapByNodeId;
    private final Set<CanvasMouseListener> canvasMouseListeners;
    private final Map<Integer, SwingUtil.CanvasImage> errorBorderById;
    private final Set<Integer> cache;
    private final int w, h;
    private Set<CopyListener> copyListeners;
    private Set<ShowMenuListener> showMenuListeners;
    private Set<LineSegment> drawLines;
    private final FpsManager fpsManager;
    private double maxFps;

    public Canvas(int w, int h) {
        this(w, h, 60);
    }

    public Canvas(int w, int h, double maxFps) {
        this.w = w;
        this.h = h;
        selectList = new LinkedList<>();
        operations = new LinkedList<>();
        polygonComponentMap = new HashMap<>();
        canvasMouseListeners = new HashSet<>();
        errorBorderById = new HashMap<>();
        polygonComponentMapByNodeId = new HashMap<>();
        cache = new HashSet<>();
        setLayout(null);
        addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {

            }

            @Override
            public void focusLost(FocusEvent e) {
                setCtrlPressed(false);
            }
        });
        fpsManager = new FpsManager();
        this.maxFps = maxFps;
    }

    public void showPopupMenu(int x, int y) {

        JPopupMenu popupMenu = new JPopupMenu();
        if (showMenuListeners == null) {
            return;
        }
        showMenuListeners.forEach(e -> e.handler(popupMenu));

        if (popupMenu.getComponents().length == 0) {
            return;
        }
        popupMenu.show(this, x, y);
    }

    public void addCanvasMouseListener(CanvasMouseListener listener) {
        canvasMouseListeners.add(listener);
    }

    public void addElement(PolygonComponent<?> obj) {

        MouseListener[] mouseListeners = getMouseListeners();
        if (mouseListeners != null) {
            for (MouseListener mouseListener : mouseListeners) {
                obj.addMouseListener(new CanvasMouseListenerRepeater(obj, mouseListener));
            }
        }

        MouseMotionListener[] mouseMotionListeners = getMouseMotionListeners();
        if (mouseMotionListeners != null) {
            for (MouseMotionListener mouseMotionListener : mouseMotionListeners) {
                obj.addMouseMotionListener(new CanvasMouseMotionListenerRepeater(obj, mouseMotionListener));
            }
        }

        obj.addSelectListener(new PolygonComponent.SelectListener() {
            @Override
            public void select() {
                synchronized (selectList) {
                    selectList.add(obj);
                }
            }

            @Override
            public void unselect() {
                synchronized (selectList) {
                    selectList.remove(obj);
                }
            }
        });

        errorBorderById.put(obj.getSrc().getId(), toCanvasPolygon(obj));
        polygonComponentMap.put(obj.getId(), obj);
        polygonComponentMapByNodeId.put(obj.getSrc().getId(), obj);
        add(obj);
        obj.setLastPoint(obj.getLocation());
    }

    public void drawLine(LineSegment line) {
        if (drawLines == null) {
            drawLines = new HashSet<>();
        }
        drawLines.add(line);
        // repaint();
    }

    public void rotateElement(Integer id, double rotate) {
        PolygonComponent<?> polygonComponent = polygonComponentMapByNodeId.get(id);
        polygonComponent.rotate(rotate);
        polygonComponent.repaint();
        errorBorderById.put(polygonComponent.getSrc().getId(), toCanvasPolygon(polygonComponent));
        // repaint();
    }

    private SwingUtil.CanvasImage toCanvasPolygon(PolygonComponent<?> obj) {
        com.symaster.common.geom.entity.Polygon clone = obj.getSrc().getSwellPolygon().clone();
        clone.move(0, 0);
        SwingUtil.CanvasImage canvasImage = SwingUtil.createCanvasImage((int) clone.getWidth() + 4, (int) clone.getHeight() + 4, BufferedImage.TYPE_4BYTE_ABGR);

        canvasImage.getGraphics().setColor(new Color(234, 92, 92, 255));
        canvasImage.getGraphics().drawPolygon(clone.toAwtPolygon());
        return canvasImage;
    }

    public Component getComponentAtPolygon(Point point) {
        List<Component> components = getComponent(point);
        for (Component component : components) {
            if (component instanceof PolygonComponent) {
                PolygonComponent<?> polygonComponent = (PolygonComponent<?>) component;
                if (polygonComponent.include(point)) {
                    return component;
                }
            }
        }
        return null;
    }

    public List<Component> getComponent(Point point) {

        List<Component> result = new ArrayList<>();
        Component[] components = getComponents();
        for (Component component : components) {
            Rectangle bounds = component.getBounds();
            if (bounds.getY() > point.getY() || bounds.getY() + bounds.getHeight() < point.getY()) {
                continue;
            }
            if (bounds.getX() > point.getX() || bounds.getX() + bounds.getWidth() < point.getX()) {
                continue;
            }
            result.add(component);
        }
        return result;
    }

    public void record(String... id) {
        if (id == null || id.length == 0) {
            OperationEntity operationEntity = new OperationEntity();
            polygonComponentMap.forEach((k, v) -> operationEntity.put(k, OperationDetailEntity.of(v)));

            if (operations.size() > 0 && operations.get(0).contrast(operationEntity)) {
                return;
            }
            operations.add(0, operationEntity);
            return;
        }
        OperationEntity operationEntity = new OperationEntity();
        for (String k : id) {
            PolygonComponent<?> polygonComponent = polygonComponentMap.get(k);
            operationEntity.put(k, OperationDetailEntity.of(polygonComponent));
        }
        if (operations.size() > 0 && operations.get(0).contrast(operationEntity)) {
            return;
        }
        operations.add(0, operationEntity);
    }

    public void rollback() {
        if (operations.size() == 0) {
            return;
        }
        OperationEntity remove = operations.remove(0);
        remove.getOperationDetailEntityMap().forEach((k, v) -> {
            PolygonComponent<?> polygonComponent = polygonComponentMap.get(k);
            v.rollback(polygonComponent);
        });
    }

    public void mouseReleased(MouseEvent e) {
        if (canvasMouseListeners.size() > 0) {
            for (CanvasMouseListener canvasMouseListener : canvasMouseListeners) {
                canvasMouseListener.mouseReleased();
            }
        }
    }

    public List<PolygonComponent<?>> getSelectList() {
        List<PolygonComponent<?>> result;
        synchronized (selectList) {
            result = new LinkedList<>(selectList);
        }
        return result;
    }

    public Point getPressedPoint() {
        return pressedPoint;
    }

    public void setPressedPoint(Point pressedPoint) {
        this.pressedPoint = pressedPoint;
    }

    public boolean isCtrlPressed() {
        return ctrlPressed;
    }

    public void setCtrlPressed(boolean ctrlPressed) {
        this.ctrlPressed = ctrlPressed;
    }

    public void selectAll() {
        for (Component component : this.getComponents()) {
            if (component instanceof PolygonComponent) {
                PolygonComponent<?> polygonComponent = (PolygonComponent<?>) component;
                polygonComponent.selectPolygon();
            }
        }
    }

    public void moveSelectAll(int offsetX, int offsetY) {
        int w = getWidth();
        int h = getHeight();

        int x01 = w;
        int y01 = h;
        int x02 = 0;
        int y02 = 0;
        Component[] components = getComponents();
        for (Component component : components) {
            if (component instanceof PolygonComponent) {
                PolygonComponent<?> component1 = (PolygonComponent<?>) component;
                if (component1.isSelect()) {
                    x01 = Math.min(component1.getX(), x01);
                    y01 = Math.min(component1.getY(), y01);
                    x02 = Math.max(component1.getX() + component1.getW(), x02);
                    y02 = Math.max(component1.getY() + component1.getH(), y02);
                }
            }
        }

        for (Component component : components) {
            if (component instanceof PolygonComponent) {
                PolygonComponent<?> component1 = (PolygonComponent<?>) component;
                if (component1.isSelect()) {
                    int x = component1.getX();
                    int y = component1.getY();

                    int minX = x - x01, minY = y - y01;
                    int maxX = w - x02 + x, maxY = h - y02 + y;

                    int newX = Math.min(Math.max(x + offsetX, minX), maxX), newY = Math.min(Math.max(y + offsetY, minY), maxY);
                    component1.setLocation(newX, newY);
                }
            }
        }
    }

    public void showErrorBorder(Integer id) {
        cache.add(id);
        // SwingUtilities.invokeLater(this::repaint);
        /*CanvasPolygon canvasPolygon = errorBorderById.get(id);
        add(canvasPolygon);
        PolygonComponent<?> polygonComponent = polygonComponentMapByNodeId.get(id);
        Point location = polygonComponent.getLocation();
        canvasPolygon.setLocation((int) (location.x - polygonComponent.getSrc().getMarge()), (int) (location.y - polygonComponent.getSrc().getMarge()));
        canvasPolygon.repaint();*/
    }

    public void closeErrorBorder() {
        cache.clear();
        /*for (CanvasPolygon value : errorBorderById.values()) {
            remove(value);
        }*/
        // SwingUtilities.invokeLater(this::repaint);
    }

    public void addCopyListener(CopyListener listener) {
        if (copyListeners == null) {
            copyListeners = new HashSet<>();
        }
        copyListeners.add(listener);
    }

    public void removeCopyListeners(CopyListener listener) {
        if (copyListeners == null) {
            return;
        }
        copyListeners.remove(listener);
    }

    public void clearCopyListeners() {
        if (copyListeners == null) {
            return;
        }
        copyListeners.clear();
    }

    public void addShowMenuListener(ShowMenuListener listener) {
        if (showMenuListeners == null) {
            showMenuListeners = new HashSet<>();
        }
        showMenuListeners.add(listener);
    }

    public void removeShowMenuListener(ShowMenuListener listener) {
        if (showMenuListeners == null) {
            return;
        }
        showMenuListeners.remove(listener);
    }

    public void clearShowMenuListener(ShowMenuListener listener) {
        if (showMenuListeners == null) {
            return;
        }
        showMenuListeners.clear();
    }

    private void drawFps(Graphics g) {
        Rectangle visibleRect = getVisibleRect();
        if (visibleRect.getHeight() > 30 && visibleRect.getWidth() > 100) {
            Graphics graphics = g.create(visibleRect.x, visibleRect.y, 100, 30);

            try {
                SwingUtil.antialiasingOn((Graphics2D) graphics);
                graphics.setFont(microsoft_yaHei);
                graphics.setColor(new Color(0, 0, 0, 255));
                graphics.drawString(String.format("Fps:%s", Math.round(fpsManager.getFps())), 3, 15);
            } finally {
                graphics.dispose();
            }

        }
    }

    @Override
    public void paint(Graphics g) {
        if (!isShowing()) {
            return;
        }

        super.paint(g);

        g.setColor(new Color(78, 157, 231));
        g.drawRect(0, 0, w, h);

        if (!cache.isEmpty()) {
            for (Integer id : cache) {
                SwingUtil.CanvasImage canvasImage = errorBorderById.get(id);
                PolygonComponent<?> polygonComponent = polygonComponentMapByNodeId.get(id);
                Point location = polygonComponent.getLocation();
                double marge = polygonComponent.getSrc().getMarge();

                int x = (int) (location.x - marge);
                int y = (int) (location.y - marge);

                Image image = canvasImage.getImage();

                Graphics graphics = g.create(x, y, image.getWidth(null), image.getHeight(null));
                try {
                    graphics.drawImage(image, 0, 0, null);
                } finally {
                    graphics.dispose();
                }

                // g.drawImage(canvasImage.getImage(), x, y, null);
            }
        }

        drawMessage(g);

        if (drawLines != null && !drawLines.isEmpty()) {
            for (LineSegment drawLine : drawLines) {
                if (drawLine.getColor() != null) {
                    g.setColor(drawLine.getColor());
                } else {
                    g.setColor(drawLineDefaultColor);
                }
                com.symaster.common.geom.entity.Point startPoint = drawLine.getStartPoint();
                com.symaster.common.geom.entity.Point endPoint = drawLine.getEndPoint();
                g.drawLine(startPoint.getXasInt(), startPoint.getYasInt(), endPoint.getXasInt(), endPoint.getYasInt());
            }
        }

        // drawFps(g);
        fpsManager.paintRunnable(30, this::repaint);
    }

    private void drawMessage(Graphics g) {
        Rectangle visibleRect = getVisibleRect();
        if (visibleRect.getWidth() > 300 && visibleRect.getHeight() > 30) {
            Graphics graphics = g.create(visibleRect.x, visibleRect.y + visibleRect.height - 30, 300, 30);

            try {
                SwingUtil.antialiasingOn((Graphics2D) graphics);
                graphics.setFont(microsoft_yaHei);
                graphics.setColor(new Color(0, 0, 0, 255));
                Point mousePosition = getMousePosition();
                if (mousePosition != null) {
                    graphics.drawString(String.format("mouse x:%s y:%s", mousePosition.x, mousePosition.y), 5, 25);
                }

                List<PolygonComponent<?>> selectList1 = getSelectList();
                if (1 == selectList1.size()) {
                    PolygonComponent<?> polygonComponent = selectList1.get(0);
                    Point location = polygonComponent.getLocation();
                    graphics.drawString(String.format("shape x:%s y:%s", location.x, location.y), 150, 25);
                }
            } finally {
                graphics.dispose();
            }
        }
    }

    public void copyListener() {
        if (copyListeners != null) {
            copyListeners.forEach(CopyListener::copy);
        }
    }

    public void pasteListener() {
        if (copyListeners != null) {
            copyListeners.forEach(CopyListener::paste);
        }
    }

    static class OperationEntity {
        private Map<String, OperationDetailEntity> operationDetailEntityMap;

        public void put(String id, OperationDetailEntity operationDetail) {
            if (operationDetailEntityMap == null) {
                operationDetailEntityMap = new HashMap<>();
            }
            operationDetailEntityMap.put(id, operationDetail);
        }

        public OperationDetailEntity get(String id) {
            if (operationDetailEntityMap == null) {
                return null;
            }
            return operationDetailEntityMap.get(id);
        }

        public Map<String, OperationDetailEntity> getOperationDetailEntityMap() {
            return operationDetailEntityMap;
        }

        public boolean contrast(OperationEntity obj) {
            if (obj == null) {
                return false;
            }
            if (obj.getOperationDetailEntityMap().size() != operationDetailEntityMap.size()) {
                return false;
            }

            boolean contrast = true;
            for (String key : operationDetailEntityMap.keySet()) {
                OperationDetailEntity operationDetailEntity = obj.getOperationDetailEntityMap().get(key);
                if (operationDetailEntity == null) {
                    return false;
                }
                if (!operationDetailEntityMap.get(key).contrast(operationDetailEntity)) {
                    contrast = false;
                }
            }
            return contrast;
        }
    }

    static class OperationDetailEntity {
        private static final int offset = 10;
        private Integer x;
        private Integer y;
        private Integer angle;
        private Date time;

        public static OperationDetailEntity of(PolygonComponent<?> component) {
            Point location = component.getLocation();
            OperationDetailEntity operationDetailEntity = new OperationDetailEntity();
            operationDetailEntity.x = location.x;
            operationDetailEntity.y = location.y;
            operationDetailEntity.time = new Date();
            operationDetailEntity.angle = 0;
            return operationDetailEntity;
        }

        public Date getTime() {
            return time;
        }

        public void setTime(Date time) {
            this.time = time;
        }

        public Integer getX() {
            return x;
        }

        public void setX(Integer x) {
            this.x = x;
        }

        public Integer getY() {
            return y;
        }

        public void setY(Integer y) {
            this.y = y;
        }

        public Integer getAngle() {
            return angle;
        }

        public void setAngle(Integer angle) {
            this.angle = angle;
        }

        public void rollback(PolygonComponent<?> polygonComponent) {
            polygonComponent.setLocation(x, y);
        }

        public boolean contrast(OperationDetailEntity operationDetailEntity) {
            int abs = Math.abs(x - operationDetailEntity.x);
            if (abs > offset) {
                return false;
            }
            int abs2 = Math.abs(y - operationDetailEntity.y);
            if (abs2 > offset) {
                return false;
            }
            int abs3 = Math.abs(angle - operationDetailEntity.angle);
            return abs3 <= offset;
        }
    }

    public interface CanvasMouseListener {
        void mouseReleased();
    }

    public interface ShowMenuListener {
        void handler(JPopupMenu popupMenu);
    }
}
